Last Updated: December 18, 2025
A coffee vending machine is an automated machine that prepares and dispenses coffee and related beverages (like espresso, cappuccino, or tea) to users with minimal human intervention.
Users typically interact with the machine by:
Internally, the machine manages ingredient inventory (coffee powder, water, milk, sugar), tracks user inputs, processes payments, and maintains operational states (e.g., idle, dispensing, out of service).
In this chapter, we will explore the low-level design of a coffee vending machine in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions and better define the scope of the system.
Here is an example of how a conversation between the candidate and the interviewer might unfold:
Candidate: Should the machine support multiple beverage types?
Interviewer: Yes, the machine should support a configurable menu of beverage types like espresso, cappuccino, latte, black coffee, etc.
Candidate: Does the system need to support payment functionality?
Interviewer: No, we can skip payment handling. You can assume this machine is used in environments like offices or homes where no payment is required.
Candidate: Should the machine allow customization, such as choosing sugar level or milk quantity?
Interviewer: Yes, users should be able to make basic customizations such as choosing sugar and milk levels per beverage.
Candidate: Should the machine track inventory of ingredients like coffee, milk, and sugar?
Interviewer: Yes, it should maintain an internal inventory for all ingredients.
Candidate: How should the system handle situations where some ingredients are unavailable?
Interviewer: The system should notify the user with an appropriate message.
Candidate: Does the machine need to support concurrent user interactions?
Interviewer: No, assume the machine handles one request at a time.
Candidate: Should the machine support an admin interface for restocking and maintenance?
Interviewer: Yes, an admin should be able to view current inventory levels and refill or reset the machine as needed.
With the scope clarified, we can now summarize the core system requirements.
Core entities are the fundamental building blocks of our system. We identify them by analyzing key nouns (e.g., beverage, ingredient, payment, inventory, customization) and actions (e.g., select, pay, prepare, refill, notify) from the functional requirements. These typically translate into classes, enums, or interfaces in an object-oriented design.
Let’s walk through the requirements and extract the relevant entities:
This points directly to a Coffee entity (an abstract base class) representing a generic beverage. Concrete subclasses like Espresso, Latte, and Cappuccino define specific coffee types.
The ability to dynamically add features like sugar or caramel syrup without altering the base Coffee class is a classic use case for the Decorator pattern. This introduces a CoffeeDecorator abstract class and concrete decorator classes like ExtraSugarDecorator and CaramelSyrupDecorator, which wrap Coffee objects to add customization. ToppingType enum helps specify these options.
This necessitates an Inventory entity to manage the stock levels of various raw materials. The raw materials themselves are represented by the Ingredient enum (COFFEE_BEANS, MILK, SUGAR, etc.). The Inventory is implemented as a Singleton to ensure a single, consistent view of stock.
The overall control and flow of the vending machine's operations are managed by the CoffeeVendingMachine entity.
Coffee (Abstract Class): The base entity for all beverages. It defines the common preparation steps and abstract methods for price and recipe, which concrete coffee types implement.Concrete Coffee Types (Espresso, Latte, Cappuccino): Subclasses of Coffee that define specific beverage recipes, prices, and unique condiment additions.Ingredient (Enum): Defines the various raw materials (e.g., COFFEE_BEANS, MILK) managed by the Inventory.ToppingType (Enum): Defines the available optional toppings for customization.Inventory: A Singleton entity responsible for managing the stock levels of all Ingredients, including checking availability and deducting used quantities.CoffeeVendingMachine: The central class and client-facing interface. It orchestrates the creation and customization of beverages, interacting with the Inventory.These core entities define the essential abstractions of a Coffee Vending Machine and will guide the structure of your low-level design and class diagrams.
This section breaks down the system's architecture into its fundamental classes, their responsibilities, and the relationships that connect them. We also explore the key design patterns that provide robustness and flexibility to the solution.
The system is composed of several types of classes, each with a distinct role.
CoffeeType: Defines the base types of coffee available (ESPRESSO, LATTE, CAPPUCCINO).Ingredient: Represents all raw materials managed by the inventory (COFFEE_BEANS, MILK, etc.).ToppingType: Defines the optional add-ons a user can select (EXTRA_SUGAR, CARAMEL_SYRUP).This design does not use simple data classes, as most objects encapsulate both state and behavior.
Coffee (Abstract Class)The base class for all beverage products.
It defines the skeleton of the preparation algorithm using the Template Method pattern, with a core prepare() method and an abstract addCondiments() "hook" for subclasses to implement.
Espresso, Latte, Cappuccino: Concrete implementations of Coffee. Each class provides a specific implementation for addCondiments(), along with its unique price and recipe.CoffeeFactoryA simple factory class responsible for creating instances of concrete Coffee types based on a CoffeeType enum.
Inventory (Singleton)A thread-safe class that manages the stock levels of all Ingredients. It provides a single, global point of access to the machine's physical inventory.
CoffeeVendingMachine (Singleton & Facade)The main class and the primary entry point for all client interactions. It acts as the Context for the State pattern, delegating all user actions to its current state object. It also acts as a Facade, simplifying the complex process of coffee creation and dispensing for the client.
The relationships between classes define the system's structure and data flow.
Espresso, Latte, Cappuccino, and the abstract CoffeeDecorator all extend the Coffee abstract class.ExtraSugarDecorator and CaramelSyrupDecorator extend the CoffeeDecorator abstract class.ReadyState and SelectingState implement the VendingMachineState interface.CoffeeVendingMachine "has-a" VendingMachineState object that defines its current behavior.CoffeeDecorator object is composed of another Coffee object (the one it wraps), forming a recursive structure.CoffeeVendingMachine is associated with the Coffee object that the user has selected.CoffeeVendingMachine (client) depends on the CoffeeFactory to create base coffee objects.CoffeeVendingMachine depends on concrete CoffeeDecorators to add toppings.CoffeeVendingMachine depends on the Inventory singleton to check for and deduct ingredients.VendingMachineStates depend on the CoffeeVendingMachine (the context) to access machine data and trigger state transitions.The lifecycle of a user transaction is managed using the State pattern.
The CoffeeVendingMachine (Context) delegates its behavior to different VendingMachineState objects (ReadyState, SelectingState, etc.). This cleanly separates state-specific logic and makes managing the user interaction flow robust and easy to extend.
This pattern is used to add toppings to a coffee dynamically. The CoffeeDecorator classes wrap a base Coffee object (or another decorator), allowing for flexible combinations of toppings. Each decorator adds its own cost, recipe ingredients, and preparation steps without altering the base coffee classes.
The abstract Coffee class defines the prepare() method, which serves as a template for making any coffee. It standardizes the algorithm (grind, brew, pour) while allowing subclasses (Latte, Cappuccino) to provide their own implementation for the addCondiments() step.
The CoffeeFactory encapsulates the logic for creating different types of Coffee. This decouples the CoffeeVendingMachine from the instantiation of concrete coffee classes, making it easy to add new coffee types in the future.
The CoffeeVendingMachine class acts as a facade. It provides a simple, high-level API (selectCoffee, insertMoney, dispenseCoffee) that hides the complex internal interactions between the factory, decorators, states, and inventory from the client.
CoffeeVendingMachine and Inventory are implemented as singletons. This is a logical choice as it models a single physical machine with a single, shared inventory, ensuring a global point of access and control.
1class CoffeeType(Enum):
2 ESPRESSO = "ESPRESSO"
3 LATTE = "LATTE"
4 CAPPUCCINO = "CAPPUCCINO"
5
6
7class Ingredient(Enum):
8 COFFEE_BEANS = "COFFEE_BEANS"
9 MILK = "MILK"
10 SUGAR = "SUGAR"
11 WATER = "WATER"
12 CARAMEL_SYRUP = "CARAMEL_SYRUP"
13
14
15class ToppingType(Enum):
16 EXTRA_SUGAR = "EXTRA_SUGAR"
17 CARAMEL_SYRUP = "CARAMEL_SYRUP"CoffeeType: Defines the base beverages.Ingredient: Inventory-managed raw materials.ToppingType: Optional add-ons for customization.Coffee (Template Method Pattern)This pattern defines the skeleton of an algorithm in a base class, deferring some steps to subclasses. This is perfect for coffee preparation, where the core steps are the same, but the condiments vary.
Coffee class defines the template method prepare() and the abstract addCondiments() step.
1class Coffee(ABC):
2 def __init__(self):
3 self.coffee_type = "Unknown Coffee"
4
5 def get_coffee_type(self) -> str:
6 return self.coffee_type
7
8 # The Template Method
9 def prepare(self):
10 print(f"\nPreparing your {self.get_coffee_type()}...")
11 self._grind_beans()
12 self._brew()
13 self.add_condiments() # The "hook" for base coffee types
14 self._pour_into_cup()
15 print(f"{self.get_coffee_type()} is ready!")
16
17 # Common steps
18 def _grind_beans(self):
19 print("- Grinding fresh coffee beans.")
20
21 def _brew(self):
22 print("- Brewing coffee with hot water.")
23
24 def _pour_into_cup(self):
25 print("- Pouring into a cup.")
26
27 # Abstract step to be implemented by subclasses
28 @abstractmethod
29 def add_condiments(self):
30 pass
31
32 @abstractmethod
33 def get_price(self) -> int:
34 pass
35
36 @abstractmethod
37 def get_recipe(self) -> Dict[Ingredient, int]:
38 passImplements the Template Method pattern for beverage preparation, allowing subclasses to override addCondiments().
The prepare() method is marked final (implicitly in this structure) to ensure subclasses cannot override the overall preparation sequence.
The addCondiments() method is the "hook" where subclasses can insert their unique logic (like adding milk for a latte) into the predefined algorithm. This promotes code reuse for the common steps.
Coffee TypesEach subclass provides its price, recipe, and custom condiments.
1class Espresso(Coffee):
2 def __init__(self):
3 super().__init__()
4 self.coffee_type = "Espresso"
5
6 def add_condiments(self):
7 pass # No extra condiments for espresso
8
9 def get_price(self) -> int:
10 return 150
11
12 def get_recipe(self) -> Dict[Ingredient, int]:
13 return {Ingredient.COFFEE_BEANS: 7, Ingredient.WATER: 30}
14
15class Cappuccino(Coffee):
16 def __init__(self):
17 super().__init__()
18 self.coffee_type = "Cappuccino"
19
20 def add_condiments(self):
21 print("- Adding steamed milk and foam.")
22
23 def get_price(self) -> int:
24 return 250
25
26 def get_recipe(self) -> Dict[Ingredient, int]:
27 return {Ingredient.COFFEE_BEANS: 7, Ingredient.WATER: 30, Ingredient.MILK: 100}
28
29class Latte(Coffee):
30 def __init__(self):
31 super().__init__()
32 self.coffee_type = "Latte"
33
34 def add_condiments(self):
35 print("- Adding steamed milk.")
36
37 def get_price(self) -> int:
38 return 220
39
40 def get_recipe(self) -> Dict[Ingredient, int]:
41 return {Ingredient.COFFEE_BEANS: 7, Ingredient.WATER: 30, Ingredient.MILK: 150}CoffeeFactoryImplements the Factory pattern to abstract coffee object creation.
1class CoffeeFactory:
2 @staticmethod
3 def create_coffee(coffee_type: CoffeeType) -> Coffee:
4 if coffee_type == CoffeeType.ESPRESSO:
5 return Espresso()
6 elif coffee_type == CoffeeType.LATTE:
7 return Latte()
8 elif coffee_type == CoffeeType.CAPPUCCINO:
9 return Cappuccino()
10 else:
11 raise ValueError(f"Unsupported coffee type: {coffee_type}")CoffeeDecorator and Topping DecoratorsImplements the Decorator pattern to layer toppings dynamically. Each decorator adjusts the recipe and final preparation behavior.
1class CoffeeDecorator(Coffee):
2 def __init__(self, coffee: Coffee):
3 super().__init__()
4 self.decorated_coffee = coffee
5
6 def get_price(self) -> int:
7 return self.decorated_coffee.get_price()
8
9 def get_recipe(self) -> Dict[Ingredient, int]:
10 return self.decorated_coffee.get_recipe()
11
12 def add_condiments(self):
13 self.decorated_coffee.add_condiments()
14
15 def prepare(self):
16 self.decorated_coffee.prepare()
17
18class CaramelSyrupDecorator(CoffeeDecorator):
19 COST = 30
20 RECIPE_ADDITION = {Ingredient.CARAMEL_SYRUP: 10}
21
22 def __init__(self, coffee: Coffee):
23 super().__init__(coffee)
24
25 def get_coffee_type(self) -> str:
26 return self.decorated_coffee.get_coffee_type() + ", Caramel Syrup"
27
28 def get_price(self) -> int:
29 return self.decorated_coffee.get_price() + self.COST
30
31 def get_recipe(self) -> Dict[Ingredient, int]:
32 new_recipe = self.decorated_coffee.get_recipe().copy()
33 for ingredient, qty in self.RECIPE_ADDITION.items():
34 new_recipe[ingredient] = new_recipe.get(ingredient, 0) + qty
35 return new_recipe
36
37 def prepare(self):
38 # First, prepare the underlying coffee
39 super().prepare()
40 # Then, add the specific step for this decorator
41 print("- Drizzling Caramel Syrup on top.")
42
43class ExtraSugarDecorator(CoffeeDecorator):
44 COST = 10
45 RECIPE_ADDITION = {Ingredient.SUGAR: 1}
46
47 def __init__(self, coffee: Coffee):
48 super().__init__(coffee)
49
50 def get_coffee_type(self) -> str:
51 return self.decorated_coffee.get_coffee_type() + ", Extra Sugar"
52
53 def get_price(self) -> int:
54 return self.decorated_coffee.get_price() + self.COST
55
56 def get_recipe(self) -> Dict[Ingredient, int]:
57 new_recipe = self.decorated_coffee.get_recipe().copy()
58 for ingredient, qty in self.RECIPE_ADDITION.items():
59 new_recipe[ingredient] = new_recipe.get(ingredient, 0) + qty
60 return new_recipe
61
62 def prepare(self):
63 super().prepare()
64 print("- Stirring in Extra Sugar.")Decorators allow for flexible combinations. A client can create a Latte, wrap it with ExtraSugarDecorator, and then wrap that result with CaramelSyrupDecorator. The final object will have the combined price, recipe, and preparation steps of all components.
VendingMachineState Interface and Concrete StatesImplements the State pattern to handle user interaction flow:
1class VendingMachineState(ABC):
2 @abstractmethod
3 def select_coffee(self, machine: 'CoffeeVendingMachine', coffee: Coffee):
4 pass
5
6 @abstractmethod
7 def insert_money(self, machine: 'CoffeeVendingMachine', amount: int):
8 pass
9
10 @abstractmethod
11 def dispense_coffee(self, machine: 'CoffeeVendingMachine'):
12 pass
13
14 @abstractmethod
15 def cancel(self, machine: 'CoffeeVendingMachine'):
16 pass
17
18
19class ReadyState(VendingMachineState):
20 def select_coffee(self, machine: 'CoffeeVendingMachine', coffee: Coffee):
21 machine.set_selected_coffee(coffee)
22 machine.set_state(SelectingState())
23 print(f"{coffee.get_coffee_type()} selected. Price: {coffee.get_price()}")
24
25 def insert_money(self, machine: 'CoffeeVendingMachine', amount: int):
26 print("Please select a coffee first.")
27
28 def dispense_coffee(self, machine: 'CoffeeVendingMachine'):
29 print("Please select and pay first.")
30
31 def cancel(self, machine: 'CoffeeVendingMachine'):
32 print("Nothing to cancel.")
33
34
35class SelectingState(VendingMachineState):
36 def select_coffee(self, machine: 'CoffeeVendingMachine', coffee: Coffee):
37 print("Already selected. Please pay or cancel.")
38
39 def insert_money(self, machine: 'CoffeeVendingMachine', amount: int):
40 machine.set_money_inserted(machine.get_money_inserted() + amount)
41 print(f"Inserted {amount}. Total: {machine.get_money_inserted()}")
42 if machine.get_money_inserted() >= machine.get_selected_coffee().get_price():
43 machine.set_state(PaidState())
44
45 def dispense_coffee(self, machine: 'CoffeeVendingMachine'):
46 print("Please insert enough money first.")
47
48 def cancel(self, machine: 'CoffeeVendingMachine'):
49 print(f"Transaction cancelled. Refunding {machine.get_money_inserted()}")
50 machine.reset()
51 machine.set_state(ReadyState())
52
53
54class PaidState(VendingMachineState):
55 def select_coffee(self, machine: 'CoffeeVendingMachine', coffee: Coffee):
56 print("Already paid. Please dispense or cancel.")
57
58 def insert_money(self, machine: 'CoffeeVendingMachine', amount: int):
59 machine.set_money_inserted(machine.get_money_inserted() + amount)
60 print(f"Additional {amount} inserted. Total: {machine.get_money_inserted()}")
61
62 def dispense_coffee(self, machine: 'CoffeeVendingMachine'):
63 inventory = Inventory.get_instance()
64 coffee = machine.get_selected_coffee()
65
66 if not inventory.has_ingredients(coffee.get_recipe()):
67 print("Sorry, we are out of ingredients. Refunding your money.")
68 print(f"Refunding {machine.get_money_inserted()}")
69 machine.reset()
70 machine.set_state(OutOfIngredientState())
71 return
72
73 # Deduct ingredients and prepare coffee
74 inventory.deduct_ingredients(coffee.get_recipe())
75 coffee.prepare()
76
77 # Calculate change
78 change = machine.get_money_inserted() - coffee.get_price()
79 if change > 0:
80 print(f"Here's your change: {change}")
81
82 machine.reset()
83 machine.set_state(ReadyState())
84
85 def cancel(self, machine: 'CoffeeVendingMachine'):
86 print(f"Transaction cancelled. Refunding {machine.get_money_inserted()}")
87 machine.reset()
88 machine.set_state(ReadyState())
89
90
91class OutOfIngredientState(VendingMachineState):
92 def select_coffee(self, machine: 'CoffeeVendingMachine', coffee: Coffee):
93 print("Sorry, we are sold out.")
94
95 def insert_money(self, machine: 'CoffeeVendingMachine', amount: int):
96 print("Sorry, we are sold out. Money refunded.")
97
98 def dispense_coffee(self, machine: 'CoffeeVendingMachine'):
99 print("Sorry, we are sold out.")
100
101 def cancel(self, machine: 'CoffeeVendingMachine'):
102 print(f"Refunding {machine.get_money_inserted()}")
103 machine.reset()
104 machine.set_state(ReadyState())Each state class handles user actions appropriately. In ReadyState, only selectCoffee is valid. In SelectingState, insertMoney and cancel are valid. This eliminates a massive if/else or switch block in the main machine class.
Manages the stock levels of all ingredients.
1class Inventory:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __new__(cls):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance._initialized = False
11 return cls._instance
12
13 def __init__(self):
14 if not self._initialized:
15 self._stock: Dict[Ingredient, int] = {}
16 self._lock = threading.Lock()
17 self._initialized = True
18
19 @classmethod
20 def get_instance(cls):
21 return cls()
22
23 def add_stock(self, ingredient: Ingredient, quantity: int):
24 self._stock[ingredient] = self._stock.get(ingredient, 0) + quantity
25
26 def has_ingredients(self, recipe: Dict[Ingredient, int]) -> bool:
27 return all(self._stock.get(ingredient, 0) >= quantity
28 for ingredient, quantity in recipe.items())
29
30 def deduct_ingredients(self, recipe: Dict[Ingredient, int]):
31 with self._lock:
32 if not self.has_ingredients(recipe):
33 print("Not enough ingredients to make coffee.")
34 return
35
36 for ingredient, quantity in recipe.items():
37 self._stock[ingredient] -= quantity
38
39 def print_inventory(self):
40 print("--- Current Inventory ---")
41 for ingredient, quantity in self._stock.items():
42 print(f"{ingredient.value}: {quantity}")
43 print("-------------------------")CoffeeVendingMachine (Singleton + Context)This is the main class that clients interact with.
1class CoffeeVendingMachine:
2 _instance = None
3 _lock = threading.Lock()
4
5 def __new__(cls):
6 if cls._instance is None:
7 with cls._lock:
8 if cls._instance is None:
9 cls._instance = super().__new__(cls)
10 cls._instance._initialized = False
11 return cls._instance
12
13 def __init__(self):
14 if not self._initialized:
15 self._state = ReadyState()
16 self._selected_coffee: Coffee = None
17 self._money_inserted = 0
18 self._initialized = True
19
20 @classmethod
21 def get_instance(cls):
22 return cls()
23
24 def select_coffee(self, coffee_type: CoffeeType, toppings: List[ToppingType]):
25 # 1. Create the base coffee using the factory
26 coffee = CoffeeFactory.create_coffee(coffee_type)
27
28 # 2. Wrap it with decorators
29 for topping in toppings:
30 if topping == ToppingType.EXTRA_SUGAR:
31 coffee = ExtraSugarDecorator(coffee)
32 elif topping == ToppingType.CARAMEL_SYRUP:
33 coffee = CaramelSyrupDecorator(coffee)
34
35 # Let the state handle the rest
36 self._state.select_coffee(self, coffee)
37
38 def insert_money(self, amount: int):
39 self._state.insert_money(self, amount)
40
41 def dispense_coffee(self):
42 self._state.dispense_coffee(self)
43
44 def cancel(self):
45 self._state.cancel(self)
46
47 # Getters and Setters used by State objects
48 def set_state(self, state: VendingMachineState):
49 self._state = state
50
51 def get_state(self) -> VendingMachineState:
52 return self._state
53
54 def set_selected_coffee(self, coffee: Coffee):
55 self._selected_coffee = coffee
56
57 def get_selected_coffee(self) -> Coffee:
58 return self._selected_coffee
59
60 def set_money_inserted(self, amount: int):
61 self._money_inserted = amount
62
63 def get_money_inserted(self) -> int:
64 return self._money_inserted
65
66 def reset(self):
67 self._selected_coffee = None
68 self._money_inserted = 0
69The machine provides a simple, high-level API (selectCoffee, insertMoney) that hides the internal complexity of factories, decorators, states, and inventory.
CoffeeVendingMachineDemoThe demo class validates the entire system by simulating various user scenarios.
1class CoffeeVendingMachineDemo:
2 @staticmethod
3 def main():
4 machine = CoffeeVendingMachine.get_instance()
5 inventory = Inventory.get_instance()
6
7 # Initial setup: Refill inventory
8 print("=== Initializing Vending Machine ===")
9 inventory.add_stock(Ingredient.COFFEE_BEANS, 50)
10 inventory.add_stock(Ingredient.WATER, 500)
11 inventory.add_stock(Ingredient.MILK, 200)
12 inventory.add_stock(Ingredient.SUGAR, 100)
13 inventory.add_stock(Ingredient.CARAMEL_SYRUP, 50)
14 inventory.print_inventory()
15
16 # Scenario 1: Successful Purchase of a Latte
17 print("\n--- SCENARIO 1: Buy a Latte (Success) ---")
18 machine.select_coffee(CoffeeType.LATTE, [])
19 machine.insert_money(200)
20 machine.insert_money(50) # Total 250, price is 220
21 machine.dispense_coffee()
22 inventory.print_inventory()
23
24 # Scenario 2: Purchase with Insufficient Funds & Cancellation
25 print("\n--- SCENARIO 2: Buy Espresso (Insufficient Funds & Cancel) ---")
26 machine.select_coffee(CoffeeType.ESPRESSO, [])
27 machine.insert_money(100) # Price is 150
28 machine.dispense_coffee() # Should fail
29 machine.cancel() # Should refund 100
30 inventory.print_inventory() # Should be unchanged
31
32 # Scenario 3: Attempt to Buy with Insufficient Ingredients
33 print("\n--- SCENARIO 3: Buy Cappuccino (Out of Milk) ---")
34 inventory.print_inventory()
35 machine.select_coffee(CoffeeType.CAPPUCCINO, [ToppingType.CARAMEL_SYRUP, ToppingType.EXTRA_SUGAR])
36 machine.insert_money(300)
37 machine.dispense_coffee() # Should fail and refund
38 inventory.print_inventory()
39
40 # Refill and final test
41 print("\n--- REFILLING AND FINAL TEST ---")
42 inventory.add_stock(Ingredient.MILK, 200)
43 inventory.print_inventory()
44 machine.select_coffee(CoffeeType.LATTE, [ToppingType.CARAMEL_SYRUP])
45 machine.insert_money(250)
46 machine.dispense_coffee()
47 inventory.print_inventory()
48
49if __name__ == "__main__":
50 CoffeeVendingMachineDemo.main()Which responsibility is BEST handled by the Inventory entity in a coffee vending machine design?
No comments yet. Be the first to comment!